
import datetime
import threading
import time
import copy

import balancer.log as lg

class AlarmTime:
    """Indicates the day of the week and second that the schedular should wake up at. """
    
    def __init__(self, day_of_week, hour, minute):
        self.__day = day_of_week % 7
        self.__hour = hour % 24
        self.__minute = minute % 60

    DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

    def __str__(self):
        return "%s at %s:%02i" % (
                AlarmTime.DAYS[self.__day],
                self.__hour, self.__minute
        )

    def seconds_away(self, currentTime):
        """Returns the number of seconds between the current time and the 
            next instance of the receiver"""
        away_days = self.__day - currentTime.weekday()
        away_hours = self.__hour - currentTime.hour
        away_minutes = self.__minute - currentTime.minute
        away_seconds = currentTime.second

        t = (away_days * 86400) + (away_hours * 3600) + (away_minutes * 60) - away_seconds
        if t < 0:
            t += 7 * 86400

        return t

    def get_day(self):
        return self.__day

    def get_hour(self):
        return self.__hour

    def get_minute(self):
        return self.__minute


class DayOfWeekScheduler:
    """Schedules updates."""

    def __init__(self, gm, cb):
        """Create a new scheduler. Will trigger the callback at the nearest time."""
        self.__unthreaded_times = [] # An unthreaded version of alarms that isn't protected by the lock
        self.__gm = gm

        self.__lock = threading.RLock() # Lock protecting the following variables
        self.__times = [] # The alarms we're going to run
        self.__cb = cb
        self.__thread = None # Thread that wakes up and run the callback
        self.__next_run = None # The datetime of the next run


    def days_away(self, alarm):
        """Returns an english string describing the distance from now"""
        away = alarm.seconds_away(datetime.datetime.now())

        days = away // 86400
        hours = (away - (days * 86400)) // 3600
        minutes = (away - (days * 86400) - (hours * 3600)) // 60

        def s(n):
            """Compute pluralization for a number."""
            if n == 1:
                return ''
            else:
                return 's'

        return "%s day%s, %s hour%s, %s minute%s" % (
                days, s(days), hours, s(hours), minutes, s(minutes)
        )


    def add(self, alarm):
        """Add an alarm time. """
        self.__lock.acquire()
        try:
            self.__times.append(alarm)
            self.__recompute()
            c = copy.copy(self.__times)
            self.__unthreaded_times = c
        finally:
            self.__lock.release()


    def __recompute(self):
        closest = 8 * 86400
        now = datetime.datetime.now()

        self.__lock.acquire()
        try:
            if self.__thread:
                self.__thread.cancel()
            # Calculate the nearest time
            for alarm in self.__times:
                seconds_away = alarm.seconds_away(now)
                closest = min(closest, seconds_away)

            # Reset the timer thread
            self.__thread = threading.Timer(closest, self.__execute_cb)
            self.__thread.start()

            # Update the next run time
            self.__next_run = now + datetime.timedelta(seconds=closest)
            self.__gm.log().log(None, lg.SCHEDULE_NEXT, "Time scheduled: " + str(self.__next_run))
        finally:
            self.__lock.release()

        return closest


    def get_next_time(self):
        """Get the next time to run as a datetime"""
        self.__lock.acquire()
        try:
            return self.__next_run
        finally:
            self.__lock.release()

    def __execute_cb(self):
        """Run the callback."""
        try:
            self.__cb()
        finally:
            time.sleep(2) # Prevent ourself from rerunning the same callback within this second
            self.__recompute()

    def get_alarms(self):
        """Return the alarms used by the scheduler"""
        return self.__unthreaded_times

    def set_alarms(self, alarms, store=True):
        """Set the alarms all at once. The alarms must be zero or more AlarmTime instances. store indicates whether or not the alarms should be written into our configuration."""
        self.__lock.acquire()
        try:
            self.__times = alarms
            self.__unthreaded_times = copy.copy(self.__times)
            self.__recompute()
            for alarm in alarms:
                print alarm
        finally:
            self.__lock.release()

        if store:
            self.__save_config(alarms)


    def __save_config(self, alarms):
        """Store the AlarmTimes that we're running."""
        times = []

        for alarm in alarms:
            times.append([alarm.get_day(), alarm.get_hour(), alarm.get_minute()])

        self.__gm.save_schedule({'alarms' : times})


    def load_config(self, config):
        """Reinflate the alarms we're running."""
        if not config['alarms']:
            return

        alarms = []
        for row in config['alarms']:
            alarms.append(AlarmTime(row[0], row[1], row[2]))

        self.set_alarms(alarms, store=False)


def fire():
    print "Fire!"

if __name__ == "__main__":
    t = AlarmTime(5, 19, 14)
    s = DayOfWeekScheduler(fire)
    print t
    s.add(t)
    time.sleep(3600)
